/*
 *   SPDX-FileCopyrightText: 2012 Daniel Nicoletti <dantti12@gmail.com>
 *   SPDX-FileCopyrightText: 2012-2014 Daniel Vrátil <dvratil@redhat.com>
 *
 *   SPDX-License-Identifier: LGPL-2.1-or-later
 */

#include "edid.h"
#include "kscreen_debug_edid.h"

#include <math.h>

#include <QCryptographicHash>
#include <QFile>
#include <QStringBuilder>
#include <QStringList>
#include <QStandardPaths>

extern "C" {
#include <libdisplay-info/edid.h>
#include <libdisplay-info/info.h>
}

#define GCM_EDID_OFFSET_PNPID 0x08
#define GCM_EDID_OFFSET_SERIAL 0x0c
#define GCM_EDID_OFFSET_SIZE 0x15
#define GCM_EDID_OFFSET_GAMMA 0x17
#define GCM_EDID_OFFSET_DATA_BLOCKS 0x36
#define GCM_EDID_OFFSET_LAST_BLOCK 0x6c
#define GCM_EDID_OFFSET_EXTENSION_BLOCK_COUNT 0x7e

#define GCM_DESCRIPTOR_DISPLAY_PRODUCT_NAME 0xfc
#define GCM_DESCRIPTOR_DISPLAY_PRODUCT_SERIAL_NUMBER 0xff
#define GCM_DESCRIPTOR_COLOR_MANAGEMENT_DATA 0xf9
#define GCM_DESCRIPTOR_ALPHANUMERIC_DATA_STRING 0xfe
#define GCM_DESCRIPTOR_COLOR_POINT 0xfb

#define PNP_IDS "/usr/share/hwdata/pnp.ids"

using namespace KScreen;

struct CDeleter
{
    template<typename T>
    void operator()(T *ptr)
    {
        if (ptr) {
            free(ptr);
        }
    }
};

template<typename T>
using UniqueCPtr = std::unique_ptr<T, CDeleter>;

class Q_DECL_HIDDEN Edid::Private
{
public:
    Private()
        : valid(false)
        , width(0)
        , height(0)
        , gamma(0)
    {
    }

    Private(const Private &other)
        : valid(other.valid)
        , monitorName(other.monitorName)
        , vendorName(other.vendorName)
        , serialNumber(other.serialNumber)
        , eisaId(other.eisaId)
        , checksum(other.checksum)
        , pnpId(other.pnpId)
        , width(other.width)
        , height(other.height)
        , gamma(other.gamma)
        , red(other.red)
        , green(other.green)
        , blue(other.blue)
        , white(other.white)
    {
    }

    bool parse(const QByteArray &data);
    int edidGetBit(int in, int bit) const;
    int edidGetBits(int in, int begin, int end) const;
    float edidDecodeFraction(int high, int low) const;

    bool valid;
    QString monitorName;
    QString vendorName;
    QString serialNumber;
    QString eisaId;
    QString checksum;
    QString pnpId;
    uint width;
    uint height;
    qreal gamma;
    QQuaternion red;
    QQuaternion green;
    QQuaternion blue;
    QQuaternion white;
};

Edid::Edid()
    : QObject()
    , d(new Private())
{
}

Edid::Edid(const QByteArray &data, QObject *parent)
    : QObject(parent)
    , d(new Private())
{
    d->parse(data);
}

Edid::Edid(Edid::Private *dd)
    : QObject()
    , d(dd)
{
}

Edid::~Edid()
{
    delete d;
}

Edid *Edid::clone() const
{
    return new Edid(new Private(*d));
}

bool Edid::isValid() const
{
    return d->valid;
}

QString Edid::deviceId(const QString &fallbackName) const
{
    QString id = QStringLiteral("xrandr");
    // if no info was added check if the fallbacName is provided
    if (vendor().isNull() && name().isNull() && serial().isNull()) {
        if (!fallbackName.isEmpty()) {
            id.append(QLatin1Char('-') % fallbackName);
        } else {
            // all info we have are empty strings
            id.append(QLatin1String("-unknown"));
        }
    } else if (d->valid) {
        if (!vendor().isNull()) {
            id.append(QLatin1Char('-') % vendor());
        }
        if (!name().isNull()) {
            id.append(QLatin1Char('-') % name());
        }
        if (!serial().isNull()) {
            id.append(QLatin1Char('-') % serial());
        }
    }

    return id;
}

QString Edid::name() const
{
    if (d->valid) {
        return d->monitorName;
    }
    return QString();
}

QString Edid::vendor() const
{
    if (d->valid) {
        return d->vendorName;
    }
    return QString();
}

QString Edid::serial() const
{
    if (d->valid) {
        return d->serialNumber;
    }
    return QString();
}

QString Edid::eisaId() const
{
    if (d->valid) {
        return d->eisaId;
    }
    return QString();
}

QString Edid::hash() const
{
    if (d->valid) {
        return d->checksum;
    }
    return QString();
}

QString Edid::pnpId() const
{
    if (d->valid) {
        return d->pnpId;
    }
    return QString();
}

uint Edid::width() const
{
    return d->width;
}

uint Edid::height() const
{
    return d->height;
}

qreal Edid::gamma() const
{
    return d->gamma;
}

QQuaternion Edid::red() const
{
    return d->red;
}

QQuaternion Edid::green() const
{
    return d->green;
}

QQuaternion Edid::blue() const
{
    return d->blue;
}

QQuaternion Edid::white() const
{
    return d->white;
}

static QByteArray parsePnpId(const uint8_t *data)
{
    // Decode PNP ID from three 5 bit words packed into 2 bytes:
    //
    // | Byte |        Bit                    |
    // |      | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
    // ----------------------------------------
    // |  1   | 0)| (4| 3 | 2 | 1 | 0)| (4| 3 |
    // |      | * |    Character 1    | Char 2|
    // ----------------------------------------
    // |  2   | 2 | 1 | 0)| (4| 3 | 2 | 1 | 0)|
    // |      | Character2|      Character 3  |
    // ----------------------------------------
    const uint offset = 0x8;

    char pnpId[4];
    pnpId[0] = 'A' + ((data[offset + 0] >> 2) & 0x1f) - 1;
    pnpId[1] = 'A' + (((data[offset + 0] & 0x3) << 3) | ((data[offset + 1] >> 5) & 0x7)) - 1;
    pnpId[2] = 'A' + (data[offset + 1] & 0x1f) - 1;
    pnpId[3] = '\0';

    return QByteArray(pnpId);
}

static QByteArray parseEisaId(const uint8_t *data)
{
    for (int i = 72; i <= 108; i += 18) {
        // Skip the block if it isn't used as monitor descriptor.
        if (data[i]) {
            continue;
        }
        if (data[i + 1]) {
            continue;
        }

        // We have found the EISA ID, it's stored as ASCII.
        if (data[i + 3] == 0xfe) {
            return QByteArray(reinterpret_cast<const char *>(&data[i + 5]), 12).trimmed();
        }
    }

    // If there isn't an ASCII EISA ID descriptor, try to decode PNP ID
    return parsePnpId(data);
}

static QByteArray parseVendor(const uint8_t *data)
{
    const auto pnpId = parsePnpId(data);

    // Map to vendor name
    QFile pnpFile(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("hwdata/pnp.ids")));
    if (pnpFile.exists() && pnpFile.open(QIODevice::ReadOnly)) {
        while (!pnpFile.atEnd()) {
            const auto line = pnpFile.readLine();
            if (line.startsWith(pnpId)) {
                return line.mid(4).trimmed();
            }
        }
    }

    return pnpId;
}

bool Edid::Private::parse(const QByteArray &rawData)
{
    const quint8 *data = reinterpret_cast<const quint8 *>(rawData.constData());
    int length = rawData.length();

    auto info = di_info_parse_edid(data, length);
    if (!info) {
        qCWarning(KSCREEN_EDID) << "Failed to parse EDID data";
        valid = false;
        return valid;
    }

    pnpId = parsePnpId(data);
    vendorName = parseVendor(data);
    UniqueCPtr<char> monitorSerial{di_info_get_serial(info)};
    serialNumber = QByteArray(monitorSerial.get());

    UniqueCPtr<char> model{di_info_get_model(info)};
    monitorName = QByteArray(model.get());

    eisaId = parseEisaId(data);

    /* get the size */
    width = data[GCM_EDID_OFFSET_SIZE + 0];
    height = data[GCM_EDID_OFFSET_SIZE + 1];

    /* we don't care about aspect */
    if (width == 0 || height == 0) {
        width = 0;
        height = 0;
    }

    /* get gamma */
    if (data[GCM_EDID_OFFSET_GAMMA] == 0xff) {
        gamma = 1.0;
    } else {
        gamma = data[GCM_EDID_OFFSET_GAMMA] / 100.0 + 1.0;
    }

    /* get color red */
    red.setX(edidDecodeFraction(data[0x1b], edidGetBits(data[0x19], 6, 7)));
    red.setY(edidDecodeFraction(data[0x1c], edidGetBits(data[0x19], 5, 4)));

    /* get color green */
    green.setX(edidDecodeFraction(data[0x1d], edidGetBits(data[0x19], 2, 3)));
    green.setY(edidDecodeFraction(data[0x1e], edidGetBits(data[0x19], 0, 1)));

    /* get color blue */
    blue.setX(edidDecodeFraction(data[0x1f], edidGetBits(data[0x1a], 6, 7)));
    blue.setY(edidDecodeFraction(data[0x20], edidGetBits(data[0x1a], 4, 5)));

    /* get color white */
    white.setX(edidDecodeFraction(data[0x21], edidGetBits(data[0x1a], 2, 3)));
    white.setY(edidDecodeFraction(data[0x22], edidGetBits(data[0x1a], 0, 1)));

    /* parse EDID data */
    for (uint i = GCM_EDID_OFFSET_DATA_BLOCKS; i <= GCM_EDID_OFFSET_LAST_BLOCK; i += 18) {
        /* ignore pixel clock data */
        if (data[i] != 0) {
            continue;
        }
        if (data[i + 2] != 0) {
            continue;
        }

        /* any useful blocks? */
        if (data[i + 3] == GCM_DESCRIPTOR_COLOR_POINT) {
            if (data[i + 3 + 9] != 0xff) {
                /* extended EDID block(1) which contains
                 * a better gamma value */
                gamma = (data[i + 3 + 9] / 100.0) + 1;
            }
            if (data[i + 3 + 14] != 0xff) {
                /* extended EDID block(2) which contains
                 * a better gamma value */
                gamma = (data[i + 3 + 9] / 100.0) + 1;
            }
        }
    }

    // calculate checksum
    QCryptographicHash hash(QCryptographicHash::Md5);
    hash.addData(reinterpret_cast<const char *>(data), length);
    checksum = QString::fromLatin1(hash.result().toHex());

    valid = true;
    return valid;
}

int Edid::Private::edidGetBit(int in, int bit) const
{
    return (in & (1 << bit)) >> bit;
}

int Edid::Private::edidGetBits(int in, int begin, int end) const
{
    int mask = (1 << (end - begin + 1)) - 1;

    return (in >> begin) & mask;
}

float Edid::Private::edidDecodeFraction(int high, int low) const
{
    float result = 0.0;

    high = (high << 2) | low;
    for (int i = 0; i < 10; ++i) {
        result += edidGetBit(high, i) * pow(2, i - 10);
    }
    return result;
}

